package org.apache.lucene.search;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.util.List;
import java.util.Iterator;
import java.io.IOException;

import org.apache.lucene.util.ScorerDocQueue;

/**
 * A Scorer for OR like queries, counterpart of <code>ConjunctionScorer</code>.
 * This Scorer implements {@link Scorer#skipTo(int)} and uses skipTo() on the
 * given Scorers.
 * 
 * @todo Implement score(HitCollector, int).
 */
class DisjunctionSumScorer extends Scorer {
	/** The number of subscorers. */
	private final int nrScorers;

	/** The subscorers. */
	protected final List subScorers;

	/** The minimum number of scorers that should match. */
	private final int minimumNrMatchers;

	/**
	 * The scorerDocQueue contains all subscorers ordered by their current
	 * doc(), with the minimum at the top. <br>
	 * The scorerDocQueue is initialized the first time next() or skipTo() is
	 * called. <br>
	 * An exhausted scorer is immediately removed from the scorerDocQueue. <br>
	 * If less than the minimumNrMatchers scorers remain in the scorerDocQueue
	 * next() and skipTo() return false.
	 * <p>
	 * After each to call to next() or skipTo() <code>currentSumScore</code>
	 * is the total score of the current matching doc, <code>nrMatchers</code>
	 * is the number of matching scorers, and all scorers are after the matching
	 * doc, or are exhausted.
	 */
	private ScorerDocQueue scorerDocQueue = null;
	private int queueSize = -1; // used to avoid size() method calls on
								// scorerDocQueue

	/** The document number of the current match. */
	private int currentDoc = -1;

	/** The number of subscorers that provide the current match. */
	protected int nrMatchers = -1;

	private float currentScore = Float.NaN;

	/**
	 * Construct a <code>DisjunctionScorer</code>.
	 * 
	 * @param subScorers
	 *            A collection of at least two subscorers.
	 * @param minimumNrMatchers
	 *            The positive minimum number of subscorers that should match to
	 *            match this query. <br>
	 *            When <code>minimumNrMatchers</code> is bigger than the
	 *            number of <code>subScorers</code>, no matches will be
	 *            produced. <br>
	 *            When minimumNrMatchers equals the number of subScorers, it
	 *            more efficient to use <code>ConjunctionScorer</code>.
	 */
	public DisjunctionSumScorer(List subScorers, int minimumNrMatchers) {
		super(null);

		nrScorers = subScorers.size();

		if (minimumNrMatchers <= 0) {
			throw new IllegalArgumentException(
					"Minimum nr of matchers must be positive");
		}
		if (nrScorers <= 1) {
			throw new IllegalArgumentException(
					"There must be at least 2 subScorers");
		}

		this.minimumNrMatchers = minimumNrMatchers;
		this.subScorers = subScorers;
	}

	/**
	 * Construct a <code>DisjunctionScorer</code>, using one as the minimum
	 * number of matching subscorers.
	 */
	public DisjunctionSumScorer(List subScorers) {
		this(subScorers, 1);
	}

	/**
	 * Called the first time next() or skipTo() is called to initialize
	 * <code>scorerDocQueue</code>.
	 */
	private void initScorerDocQueue() throws IOException {
		Iterator si = subScorers.iterator();
		scorerDocQueue = new ScorerDocQueue(nrScorers);
		queueSize = 0;
		while (si.hasNext()) {
			Scorer se = (Scorer) si.next();
			if (se.next()) { // doc() method will be used in scorerDocQueue.
				if (scorerDocQueue.insert(se)) {
					queueSize++;
				}
			}
		}
	}

	/**
	 * Scores and collects all matching documents.
	 * 
	 * @param hc
	 *            The collector to which all matching documents are passed
	 *            through {@link HitCollector#collect(int, float)}. <br>
	 *            When this method is used the {@link #explain(int)} method
	 *            should not be used.
	 */
	public void score(HitCollector hc) throws IOException {
		while (next()) {
			hc.collect(currentDoc, currentScore);
		}
	}

	/**
	 * Expert: Collects matching documents in a range. Hook for optimization.
	 * Note that {@link #next()} must be called once before this method is
	 * called for the first time.
	 * 
	 * @param hc
	 *            The collector to which all matching documents are passed
	 *            through {@link HitCollector#collect(int, float)}.
	 * @param max
	 *            Do not score documents past this.
	 * @return true if more matching documents may remain.
	 */
	protected boolean score(HitCollector hc, int max) throws IOException {
		while (currentDoc < max) {
			hc.collect(currentDoc, currentScore);
			if (!next()) {
				return false;
			}
		}
		return true;
	}

	public boolean next() throws IOException {
		if (scorerDocQueue == null) {
			initScorerDocQueue();
		}
		return (scorerDocQueue.size() >= minimumNrMatchers)
				&& advanceAfterCurrent();
	}

	/**
	 * Advance all subscorers after the current document determined by the top
	 * of the <code>scorerDocQueue</code>. Repeat until at least the minimum
	 * number of subscorers match on the same document and all subscorers are
	 * after that document or are exhausted. <br>
	 * On entry the <code>scorerDocQueue</code> has at least
	 * <code>minimumNrMatchers</code> available. At least the scorer with the
	 * minimum document number will be advanced.
	 * 
	 * @return true iff there is a match. <br>
	 *         In case there is a match, </code>currentDoc</code>, </code>currentSumScore</code>,
	 *         and </code>nrMatchers</code> describe the match.
	 * 
	 * @todo Investigate whether it is possible to use skipTo() when the minimum
	 *       number of matchers is bigger than one, ie. try and use the
	 *       character of ConjunctionScorer for the minimum number of matchers.
	 *       Also delay calling score() on the sub scorers until the minimum
	 *       number of matchers is reached. <br>
	 *       For this, a Scorer array with minimumNrMatchers elements might hold
	 *       Scorers at currentDoc that are temporarily popped from scorerQueue.
	 */
	protected boolean advanceAfterCurrent() throws IOException {
		do { // repeat until minimum nr of matchers
			currentDoc = scorerDocQueue.topDoc();
			currentScore = scorerDocQueue.topScore();
			nrMatchers = 1;
			do { // Until all subscorers are after currentDoc
				if (!scorerDocQueue.topNextAndAdjustElsePop()) {
					if (--queueSize == 0) {
						break; // nothing more to advance, check for last
								// match.
					}
				}
				if (scorerDocQueue.topDoc() != currentDoc) {
					break; // All remaining subscorers are after currentDoc.
				}
				currentScore += scorerDocQueue.topScore();
				nrMatchers++;
			} while (true);

			if (nrMatchers >= minimumNrMatchers) {
				return true;
			} else if (queueSize < minimumNrMatchers) {
				return false;
			}
		} while (true);
	}

	/**
	 * Returns the score of the current document matching the query. Initially
	 * invalid, until {@link #next()} is called the first time.
	 */
	public float score() throws IOException {
		return currentScore;
	}

	public int doc() {
		return currentDoc;
	}

	/**
	 * Returns the number of subscorers matching the current document. Initially
	 * invalid, until {@link #next()} is called the first time.
	 */
	public int nrMatchers() {
		return nrMatchers;
	}

	/**
	 * Skips to the first match beyond the current whose document number is
	 * greater than or equal to a given target. <br>
	 * When this method is used the {@link #explain(int)} method should not be
	 * used. <br>
	 * The implementation uses the skipTo() method on the subscorers.
	 * 
	 * @param target
	 *            The target document number.
	 * @return true iff there is such a match.
	 */
	public boolean skipTo(int target) throws IOException {
		if (scorerDocQueue == null) {
			initScorerDocQueue();
		}
		if (queueSize < minimumNrMatchers) {
			return false;
		}
		if (target <= currentDoc) {
			return true;
		}
		do {
			if (scorerDocQueue.topDoc() >= target) {
				return advanceAfterCurrent();
			} else if (!scorerDocQueue.topSkipToAndAdjustElsePop(target)) {
				if (--queueSize < minimumNrMatchers) {
					return false;
				}
			}
		} while (true);
	}

	/** @return An explanation for the score of a given document. */
	public Explanation explain(int doc) throws IOException {
		Explanation res = new Explanation();
		Iterator ssi = subScorers.iterator();
		float sumScore = 0.0f;
		int nrMatches = 0;
		while (ssi.hasNext()) {
			Explanation es = ((Scorer) ssi.next()).explain(doc);
			if (es.getValue() > 0.0f) { // indicates match
				sumScore += es.getValue();
				nrMatches++;
			}
			res.addDetail(es);
		}
		if (nrMatchers >= minimumNrMatchers) {
			res.setValue(sumScore);
			res.setDescription("sum over at least " + minimumNrMatchers
					+ " of " + subScorers.size() + ":");
		} else {
			res.setValue(0.0f);
			res.setDescription(nrMatches + " match(es) but at least "
					+ minimumNrMatchers + " of " + subScorers.size()
					+ " needed");
		}
		return res;
	}
}
